
package redeS.arquitetura;

import java.util.ArrayList;
import peersim.core.CommonState;
import peersim.core.Network;
import peersim.core.Node;
import peersim.config.Configuration;
import peersim.core.Control;
import redeS.visualizacao.P2PGraph;

/**
 * Classe inicializadora da arquitetura Gnutella.
 * Essa classe realiza as ligações da rede Gnutella.
 * @author thiago-pc
 */
public class GnutellaInitializer implements Control {
	
	/**
	 * O parâmetro node_conections representa o número de ligações que cada nó, ao entrar
	 * na rede, tentará realizar com seus vizinhos.
	 */
	public static final String PAR_NDCON = "node_conections";
	private int nodeConections;
	private final int defaultNodeConections = 2;
	
	// O número do protocolo (é calculado internamente no simulador).
	private int pid = -1;
	
	public GnutellaInitializer(String prefix){
		// Pega o valor do parâmetro nodeConections do arquivo de configuração.
		// Se o arquivo de configuração não tiver esse parâmetro ele será setado
		// com o valor padrão (veja o valor default na declaração da variável).
		nodeConections = Configuration.getInt(prefix + "." + PAR_NDCON,defaultNodeConections);
		
		if(Configuration.contains(prefix + ".protocol") ){
			pid = Configuration.getPid(prefix + ".protocol");
		}
		if(pid<0){
			System.err.println("Especifique a string identificadora do protocolo Gnutella");
			System.exit(1);
		}
		
	}
	
	/**
	 * Cada nó busca fazer um número de ligações com seus vizinhos. Esse método retorna
	 * esse número.
	 * @return int nodeConections
	 */
	public int getNodeConections() {
		return nodeConections;
	}

	/**
	 * Nesse caso esse método é responsável por construir as ligações entre os nós de forma
	 * descentralizada conforme a arquitetura Gnutella é.
	 */
	@Override
	public boolean execute() {
		
		System.err.println("Inserindo nós na rede. Aguarde...");
		int p = 0;
		double q = 0;
		
		// Para cada nó da rede serão setados nodeConections vizinhos aleatórios.
		for(int i=0; i<Network.size(); i++){
			// Pega o nó i e o protocolo Gnutella de i.
			Node current = Network.get(i);
			GnutellaProtocol currentProtocol = (GnutellaProtocol)current.getProtocol(pid);
			
			// Se achar o protocolo Gnutella (que é o que deve acontecer senão o cast vai
			// gerar uma exeção) procura-se por um vizinho n e faz-se a ligação com ele.
			// Isso se repete até que haja nodeConections ligações.
			while(currentProtocol.degree() < nodeConections){
				Node ableNeighbor = null;
				GnutellaProtocol ableNeighborProtocol = null;
				
				// O for abaixo pega o protocolo Gnutella de cada nó e vê se existe pelo
				// menos um nó, fora o i, que esteja faltando conexões: se estiver
				// ableNeighbor recebe esse nó.
				for(int j=i+1; j<Network.size(); j++){
						Node curNode = Network.get(j);
						GnutellaProtocol curProtocol = (GnutellaProtocol)curNode.getProtocol(pid);
						
						if(curProtocol.degree() < nodeConections &&	!currentProtocol.contains(curNode)){
							ableNeighbor = curNode;
							ableNeighborProtocol = curProtocol;
							break;
					}
				}
				
				// O do-while abaixo escolherá um vizinho n apropriado: ele vai preferir
				// se conectar a nós que estejam faltando conexões. A variável ableNeighbor
				// acima informa a esse laço se há nós assim. Senão é escolhido um vizinho
				// aleatório.
				if(ableNeighbor!=null){
					currentProtocol.addNeighbor(ableNeighbor);
					ableNeighborProtocol.addNeighbor(current);
				}
				else{
					int n;
					do{
						n = CommonState.r.nextInt() % Network.size();
						n = Math.abs(n);
						ableNeighbor = Network.get(n);
						ableNeighborProtocol = (GnutellaProtocol)ableNeighbor.getProtocol(pid);
					}while(n==i || currentProtocol.contains(ableNeighbor));
					
					currentProtocol.addNeighbor(ableNeighbor);
					ableNeighborProtocol.addNeighbor(current);
				}
			}
			q = i / (float)Network.size();
			q *= 100;
			System.err.printf("%2.1f%%\b\b\b\b\b\b",q);
			q = 0;
		}
		
		System.err.println("Concluído!");
		
		// Se a rede ainda não estiver totalmente conectada (muito provavelmente), as
		// componentes não-conectadas da rede são ligadas entre si para resolver esse
		// problema; para tal é gerado um grafo g (P2PGraph) para auxiliar.	
		
		P2PGraph g = new P2PGraph(pid);
		ArrayList<int[]> comp;
		
		System.err.println("Gerando rede Gnutella. Aguarde...");
		
		int k = Network.size()/(nodeConections+1)>100? 100 : Network.size()/(nodeConections+1);
		while(!g.isFullyConected()){
			// pega k componentes não-conectadas aleatórias com nodeConections cada
			comp = g.getNotConectedComponts(k,nodeConections);
			if(comp==null){
				k = k/2 > 2? k/2 : 2;
				continue;
			}
			
			for(int i=0; i<nodeConections; i++){
				for(int j=0; j<comp.size()-1; j+=2){
					Node node1 = Network.get(comp.get(j)[i]);
					Node node2 = Network.get(comp.get(j+1)[i]);
					GnutellaProtocol prot1 = (GnutellaProtocol)node1.getProtocol(pid);
					GnutellaProtocol prot2 = (GnutellaProtocol)node2.getProtocol(pid);
				
					prot1.addNeighbor(node2);
					prot2.addNeighbor(node1);
				}
			}
			
			p++;
			q = q < 98? p * 1.25 : q > 100? q : q + 0.001;
			System.err.printf("%2.2f%%\b\b\b\b\b\b\b",q);
			
		}
		System.err.println("Concluído!");
		
		return false;
	}
}
